一、patch简述
https://github.com/saelo/35c3ctf/blob/master/WebKid/webkid.patch
一个patch就足够了。patch的核心在于给操作
1 | thisObject->setStructure(vm, Structure::removePropertyTransition(vm, structure, propertyName, offset)); |
增加了一个fast path。在这个fast path中,假如delete的property是最后一个添加的就把它删去,然后把structureID恢复到上一个。
对比一下正常的行为:
patch之前的版本:
Object: 0x11cdc8580 with butterfly 0x18000fe5c8 (Structure 0x11cdf2760:[Array, {}, ArrayWithDouble, Proto:0x11cdb4090, Leaf]), StructureID: 91Object: 0x11cdc8580 with butterfly 0x18000f8468 (Structure 0x11cda7c60:[Array, {outOfLineProperty:100}, ArrayWithDouble, Proto:0x11cdb4090, Leaf]), StructureID: 282
Object: 0x11cdc8580 with butterfly 0x18000f8468 (Structure 0x11cda7cd0:[Array, {}, ArrayWithDouble, Proto:0x11cdb4090, UncacheableDictionary, Leaf]), StructureID: 283
- delete outOfLineProperty之后StructureID从282增长到283。
patch之后的版本:
Object: 0x11cdc8580 with butterfly 0xc000fe5c8 (Structure 0x11cdf2760:[Array, {}, ArrayWithDouble, Proto:0x11cdb4090, Leaf]), StructureID: 91
Object: 0x11cdc8580 with butterfly 0xc000f8468 (Structure 0x11cda7c60:[Array, {outOfLineProperty:100}, ArrayWithDouble, Proto:0x11cdb4090, Leaf]), StructureID: 282
Object: 0x11cdc8580 with butterfly 0xc000f8468 (Structure 0x11cdf2760:[Array, {}, ArrayWithDouble, Proto:0x11cdb4090]), StructureID: 91- delete outOfLineProperty之后StructureID从282回到了之前的91。
一说到structureID的改变,就想到DFG JIT中watchpoint(CodeBlockJettisoningWatchpoint)的fire是靠不同的structure来触发的,一个最简单的原语:
1 | function primitiveMaybeNotExploitable(obj) { |
DFG生成的visit()函数在最后一次执行前,structureID为91,按理说已经不包含outOfLineProperty,但由于没有触发watchpoint,代码逻辑没有发生改变,而且butterfly指针保留,仍然能获得相应的引用。这是一个问题,但应该无法做到漏洞利用。
而正常版本的deleteProperty显然会触发watchpoint,最后得出结果是undefined:
1 | Firing watchpoint 0x11c84d0a0 on visit#ETPOUh:[0x11cd784c0->0x11cd78260->0x11cd98f20, DFGFunctionCall, 35] |
栈回溯的第12条是原版代码中被fast path越过的代码。removePropertyTransition才是用来触发WatchpointSet::fireAll的函数调用,而patch中只是简单地设置了:
1 | thisObject->setStructure(vm, previous); |
二、primitive构造
WatchPointSet是通过structure对象索引的,structure对象由structureID确定,因此一个structureID对应一个WatchPointSet。在第一部分的visit()函数中,DFG插入Watchpoint时,arr的类型是ArrayWithDouble并附加一个outOfLineProperty,structureID是282。也就是说WatchPointSet对应strucutre282,那么之后变成structure91之后是没有注册WatchPointSet的,如果再次进行类型转换,仍然不会fire。
(如果struture91被注册了WatchPointSet,在654行会进行fire)
因此再给代码增加一个transition,用传统的convertDoubleToContiguous:
1 | function primitiveAddrOf(obj) { |
于是我们有了两个原语,addrof、fakeobj。
三、全局读写构造
addof、fakeobj之后,构造全局读写还是比较简单的,基本有模版可参考:
1 | let BASE32 = 0x100000000; |
漏洞利用围绕JSValue,细节此处略过。
四、CheckStructure与WatchPoint
WatchPoint在fire的时候,会找到InvalidationPoint的位置,然后通过JumpReplacement跳离不安全的DFG代码回到baseline JIT,而CheckStructure则是类似:
按照前三节的js代码,DFG IR中不存在CheckStruture。
改造一下PoC,使arr通过函数参数arg的形式传入:
1 | function primitiveAddrOf(obj) { |
生成的dfg代码稍微发生了变化,可以看到其中虽然没有CheckStructure产生,但有CheckArray:
1 | Generated DFG JIT code for visit#BFOcsO:[0x11cd784c0->0x11cd78260->0x11cd98f20, DFGFunctionCall, 15 (NeverInline)], instruction count = 15: |
1 | (lldb) register read $rax |
其中0x09这个字节对应的是IndexingType:
可以看到CheckArray与CheckStructure出现的位置类似,作用也类似。因此在这个PoC的visit()函数中,如果用参数来传递arr将会被检查出来,无法带来类型混淆。